<script setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
import { openContextMenu } from '../assets/menus';
import { triggerAction } from '../assets/hotkeys';
import VirtualList from './VirtualList.vue';
import TreeRow from './TreeRow.vue';
import FileIcon from './FileIcon.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
  root: { type: Object, required: true }, // { id, label, isDir, children? }
  loadChildren: { type: Function, default: null }, // async (node) => children[]
  indent: { type: Number, default: 12 },
  getIcon: { type: Function, default: null },
  showRoot: { type: Boolean, default: true },
  compressFolders: { type: Boolean, default: true }, // 折叠时压缩单链目录
  filterText: { type: String, default: '' }, // 简易过滤
  filterFn: { type: Function, default: null }, // (node) => boolean，自定义过滤
  enableMultiSelect: { type: Boolean, default: true },
  draggable: { type: Boolean, default: true },
  compressSeparator: { type: String, default: '/' },
  animateChanges: { type: Boolean, default: false }, // 新增：更新时开启平滑动画
});
const emit = defineEmits(['open', 'preview', 'selection-change', 'dragstart', 'dragover', 'dragleave', 'drop', 'dragend']);

const rootRef = ref(null);
const virtualListRef = ref(null);

// VS Code 风格：使用扁平可见列表渲染（无粘性目录实现）
// 后续如需虚拟列表，可在此基础上替换为窗口化渲染

const expanded = ref(new Set());
const selectedId = ref(null); // 焦点项
const selectedIds = ref(new Set()); // 多选
const lastAnchorIndex = ref(-1);
const rowHeight = 24; // 与样式行高保持一致，用于键盘滚动定位
let lastClickTime = 0;
let lastClickId = null;
const scrollTop = ref(0);
// 新增：节点索引（Phase 1）——用于 id -> 节点 的快速查找/同步
const nodeIndex = ref(new Map());

function isExpanded(id) { return expanded.value.has(String(id)); }
function toggle(node) {
  const id = String(node.id);
  if (expanded.value.has(id)) expanded.value.delete(id); else expanded.value.add(id);
}
function select(node, e, extra) {
  const idx = findIndexById(node.id);
  const isDir = node.isDir === true || node.is_dir === true || node.isFolder === true;
  // 上下文菜单
  if (extra?.context && e) {
    selectedId.value = String(node.id);
    if (!props.enableMultiSelect || (!e.ctrlKey && !e.metaKey && !e.shiftKey)) {
      selectedIds.value = new Set([String(node.id)]);
      lastAnchorIndex.value = idx;
      emit('selection-change', Array.from(selectedIds.value));
    }
    const at = { x: e.clientX, y: e.clientY };
    openContextMenu({ use: 'explorer.context' }, at, { node, selectedIds: Array.from(selectedIds.value) });
    return;
  }
  // 点击选择（支持多选）
  if (e && e.type === 'click') {
    const sid = String(node.id);
    // 简易单击预览/双击打开可在父级用事件配合实现；这里保留单击选择
    if (props.enableMultiSelect && e.shiftKey && lastAnchorIndex.value >= 0) {
      const cur = idx >= 0 ? idx : 0;
      const start = Math.min(lastAnchorIndex.value, cur);
      const end = Math.max(lastAnchorIndex.value, cur);
      const arr = effectiveFlatNodes.value;
      const set = new Set(selectedIds.value);
      for (let i = start; i <= end; i++) set.add(String(arr[i]?.node?.id));
      selectedIds.value = set;
      selectedId.value = sid;
      emit('selection-change', Array.from(selectedIds.value));
    } else if (props.enableMultiSelect && (e.ctrlKey || e.metaKey)) {
      const set = new Set(selectedIds.value);
      if (set.has(sid)) set.delete(sid); else set.add(sid);
      selectedIds.value = set;
      selectedId.value = sid;
      lastAnchorIndex.value = idx;
      emit('selection-change', Array.from(selectedIds.value));
    } else {
      selectedIds.value = new Set([sid]);
      selectedId.value = sid;
      lastAnchorIndex.value = idx;
      emit('selection-change', Array.from(selectedIds.value));
    }
    // 单击目录：展开压缩链或切换
    if (isDir && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
      Promise.resolve(expandOrToggleDir(node, !!e.altKey));
    }
    // 记录最后点击时间与 id，用于父级按需实现单击预览
    lastClickTime = Date.now();
    lastClickId = sid;
  }
}

function getChildren(node) {
  return Array.isArray(node.children) ? node.children : [];
}

async function loadChildrenIfNeeded(node) {
  if (!props.loadChildren) return;
  
  // 检查是否需要加载：未加载过（children === undefined）或者明确标记需要重新加载
  const needsLoading = !Array.isArray(node.children) || (node.__needsReload && Array.isArray(node.children));
  
  if (!needsLoading) return;
  
  try {
    // 添加加载状态标记，防止重复加载
    if (node.__loading) return;
    node.__loading = true;
    
    const kids = await props.loadChildren(node);
    node.children = Array.isArray(kids) ? kids : [];
    
    // 清除重载标记
    delete node.__needsReload;
  } catch (error) {
    console.warn('Failed to load children for node:', node.id, error);
    // 加载失败时设置为空数组，避免无限重试
    node.children = [];
  } finally {
    delete node.__loading;
  }
}

// 扁平可见节点列表
const flatNodes = computed(() => {
  const out = [];
  // 同时构建父映射，便于键盘导航
  nodeParentMap.value.clear();
  function computeCompressedLabelIfAny(n) {
    if (!props.compressFolders) return null;
    const rootId = String(n.id);
    if (expanded.value.has(rootId)) return null; // 已展开则不压缩
    if (!isDirectory(n)) return null;
    const parts = [String(n.label ?? '')];
    let current = n;
    // 仅在单子目录链上压缩，要求每层 children 已加载且唯一且为目录
    while (true) {
      const kids = getChildren(current);
      if (!Array.isArray(kids) || kids.length !== 1) break;
      const only = kids[0];
      if (!isDirectory(only)) break;
      parts.push(String(only.label ?? ''));
      current = only;
      // 不跨越已展开的下层节点；压缩显示不受影响，但避免无限深入
      if (expanded.value.has(String(current.id))) break;
    }
    if (parts.length <= 1) return null;
    return parts.join('/');
  }
  function pushNode(n, depth) {
    if (!n) return;
    nodeIndex.value.set(String(n.id), n);
    const compressed = computeCompressedLabelIfAny(n);
    out.push({ node: n, depth, displayLabel: compressed || String(n.label ?? '') });
    const isDir = n.isDir === true || n.is_dir === true || n.isFolder === true;
    if (!isDir) return;
    const id = String(n.id);
    if (!expanded.value.has(id)) return;
    const children = getChildren(n);
    if (!Array.isArray(children) || children.length === 0) return;
    for (const c of children) { nodeParentMap.value.set(String(c.id), String(n.id)); pushNode(c, depth + 1); }
  }
  if (!props.root) return out;
  if (props.showRoot) pushNode(props.root, 0);
  else {
    const kids = getChildren(props.root);
    for (const c of kids) { nodeParentMap.value.set(String(c.id), String(props.root.id)); pushNode(c, 0); }
  }
  return out;
});

// 过滤（简易）
const effectiveFlatNodes = computed(() => {
  const q = String(props.filterText || '').trim().toLowerCase();
  const matcher = props.filterFn || ((n) => String(n.label || '').toLowerCase().includes(q));
  if (!q) return flatNodes.value;
  // 过滤场景下重建父映射，保证键盘导航的向上跳转可用
  nodeParentMap.value.clear();
  // 搜索时：无视展开集，保证能看到匹配的后代；仅输出匹配节点及其祖先
  const include = new Set();
  function dfsMark(n) {
    if (!n) return false;
    const kids = getChildren(n);
    let matched = false;
    if (matcher(n)) matched = true;
    if (Array.isArray(kids)) {
      for (const c of kids) {
        if (dfsMark(c)) {
          include.add(String(n.id));
          matched = true;
        }
      }
    }
    if (matched) include.add(String(n.id));
    return matched;
  }
  if (props.root) dfsMark(props.root);
  const out = [];
  function pushAll(n, depth, parentId) {
    if (!n) return;
    const id = String(n.id);
    if (!include.has(id)) return;
    if (parentId != null) nodeParentMap.value.set(id, String(parentId));
    // 重用压缩显示
    const display = (function() {
      const isDir = n.isDir === true || n.is_dir === true || n.isFolder === true;
      if (!isDir) return String(n.label ?? '');
      const kids = getChildren(n);
      if (!Array.isArray(kids) || kids.length !== 1) return String(n.label ?? '');
      // 不理会 expanded，在搜索时也进行压缩
      const parts = [String(n.label ?? '')];
      let cur = n;
      while (true) {
        const k = getChildren(cur);
        if (!Array.isArray(k) || k.length !== 1) break;
        const only = k[0];
        if (!isDirectory(only)) break;
        parts.push(String(only.label ?? ''));
        cur = only;
      }
      return parts.join(props.compressSeparator || '/');
    })();
    nodeIndex.value.set(String(n.id), n);
    out.push({ node: n, depth, displayLabel: display });
    const kids = getChildren(n);
    if (Array.isArray(kids)) for (const c of kids) pushAll(c, depth + 1, id);
  }
  if (props.root) {
    if (props.showRoot) pushAll(props.root, 0);
    else for (const c of getChildren(props.root) || []) pushAll(c, 0, props.root.id);
  }
  return out;
});

// 提供 id->可见行索引的 O(1) 查找
const indexById = computed(() => {
  const map = new Map();
  const arr = effectiveFlatNodes.value;
  for (let i = 0; i < arr.length; i++) map.set(String(arr[i]?.node?.id), i);
  return map;
});

const nodeParentMap = ref(new Map());

function isDirectory(n) {
  return n && (n.isDir === true || n.is_dir === true || n.isFolder === true);
}

// Inline create/rename state
const editingNodeId = ref(null);
const editingMode = ref(null); // 'new-file' | 'new-folder' | 'rename'
const editingLabel = ref('');
const editingParentId = ref(null);
const editingInputEl = ref(null);
function setEditingInput(el) {
  editingInputEl.value = el;
  if (el) {
    nextTick(() => {
      el.focus();
      el.select();
    });
  }
}

// 重新加载已展开目录的子节点（在外部刷新数据源后调用，避免“看起来被收起”）
async function rehydrateExpanded() {
  try {
    async function forceReload(n) {
      if (!props.loadChildren) return;
      const kids = await props.loadChildren(n);
      n.children = Array.isArray(kids) ? kids : [];
    }
    async function dfs(n) {
      if (!n) return;
      if (isDirectory(n)) {
        const id = String(n.id);
        if (expanded.value.has(id)) {
          await forceReload(n);
          const kids = getChildren(n);
          if (Array.isArray(kids)) {
            for (const c of kids) await dfs(c);
          }
        }
      }
    }
    if (props.root) await dfs(props.root);
  } catch {}
}

function findNodeById(node, id) {
  if (!node) return null;
  if (String(node.id) === String(id)) return node;
  const kids = Array.isArray(node.children) ? node.children : [];
  for (const c of kids) { const f = findNodeById(c, id); if (f) return f; }
  return null;
}

async function ensureExpandedAndLoaded(dirNode) {
  const id = String(dirNode.id);
  if (!expanded.value.has(id)) expanded.value.add(id);
  await loadChildrenIfNeeded(dirNode);
}

async function beginInline(mode, node) {
  try {
    editingMode.value = mode;
    if (mode === 'rename') {
      editingNodeId.value = String(node.id);
      editingLabel.value = String(node.label || '');
      return;
    }
    // new-file / new-folder
    const targetDirId = (isDirectory(node)) ? String(node.id) : String(nodeParentMap.value.get(String(node.id)) || props.root?.id || '');
    const parent = findNodeById(props.root, targetDirId) || props.root;
    if (!parent) return;
    await ensureExpandedAndLoaded(parent);
    if (!Array.isArray(parent.children)) parent.children = [];
    const tempId = `__new__:${Date.now()}`;
    const tempNode = { id: tempId, label: '', isDir: mode === 'new-folder', __temp: true };
    parent.children.unshift(tempNode);
    editingParentId.value = String(parent.id);
    editingNodeId.value = tempId;
    editingLabel.value = '';
    selectedId.value = tempId;
    await nextTick();
    try { const el = editingInputEl.value; if (el) { el.focus(); el.select?.(); } } catch {}
  } catch {}
}

async function commitInline() {
  const mode = editingMode.value;
  const id = editingNodeId.value;
  const label = String(editingLabel.value || '').trim();
  if (!mode || !id) return cancelInline();
  if (!label) return cancelInline();
  try {
    if (mode === 'rename') {
      const node = findNodeById(props.root, id);
      if (!node) return cancelInline();
      const oldId = String(node.id);
      const { dirname, join } = await import('@tauri-apps/api/path');
      const baseDir = await dirname(String(node.id));
      const newPath = await join(baseDir, label);
      const { invoke } = await import('@tauri-apps/api/core');
      await invoke('fs_rename', { from: String(node.id), to: newPath });
      node.id = newPath;
      node.label = label;
      // 更新 NodeIndex
      try { nodeIndex.value.delete(oldId); nodeIndex.value.set(String(newPath), node); } catch {}
      editingNodeId.value = null; editingMode.value = null; editingLabel.value = ''; editingParentId.value = null;
      return;
    }
    // new
    const parent = findNodeById(props.root, editingParentId.value) || props.root;
    if (!parent) return cancelInline();
    const { join } = await import('@tauri-apps/api/path');
    const target = await join(String(parent.id), label);
    const { invoke } = await import('@tauri-apps/api/core');
    if (editingMode.value === 'new-folder') await invoke('fs_mkdir', { path: target, recursive: true });
    else await invoke('fs_write_text', { path: target, content: '' });
    const list = Array.isArray(parent.children) ? parent.children : [];
    const idx = list.findIndex(n => String(n.id) === String(editingNodeId.value));
    if (idx >= 0) {
      const created = { id: target, label, isDir: editingMode.value === 'new-folder' };
      list.splice(idx, 1, created);
      try { nodeIndex.value.set(String(target), created); } catch {}
    }
    const newId = target;
    editingNodeId.value = null; editingMode.value = null; editingLabel.value = ''; editingParentId.value = null;
    if (mode === 'new-file') emit('open', { id: newId });
  } catch {
    cancelInline();
  }
}

function cancelInline() {
  try {
    if (editingMode.value === 'new-file' || editingMode.value === 'new-folder') {
      const parent = findNodeById(props.root, editingParentId.value) || props.root;
      if (parent && Array.isArray(parent.children)) {
        const idx = parent.children.findIndex(n => String(n.id) === String(editingNodeId.value));
        if (idx >= 0) parent.children.splice(idx, 1);
      }
    }
  } catch {}
  editingNodeId.value = null; editingMode.value = null; editingLabel.value = ''; editingParentId.value = null;
}

// 标记节点需要重新加载（用于外部调用）
function markNodeForReload(nodeId) {
  const node = findNodeById(props.root, nodeId);
  if (node) {
    node.__needsReload = true;
  }
}

// 强制重新加载节点的子节点
async function forceReloadNode(nodeId) {
  const node = findNodeById(props.root, nodeId);
  if (node && props.loadChildren) {
    try {
      node.__loading = true;
      const kids = await props.loadChildren(node);
      node.children = Array.isArray(kids) ? kids : [];
      delete node.__needsReload;
    } catch (error) {
      console.warn('Failed to force reload node:', nodeId, error);
      node.children = [];
    } finally {
      delete node.__loading;
    }
  }
}

defineExpose({ beginInline, rehydrateExpanded, markNodeForReload, forceReloadNode });

async function expandCompressedChain(node) {
  if (!props.compressFolders) return false;
  if (!isDirectory(node)) return false;
  let changed = false;
  let current = node;
  // 展开自上而下的单子目录链，直到遇到非单链或已展开层
  while (true) {
    const id = String(current.id);
    if (expanded.value.has(id)) break;
    await loadChildrenIfNeeded(current);
    const kids = getChildren(current);
    if (!Array.isArray(kids) || kids.length !== 1 || !isDirectory(kids[0])) break;
    expanded.value.add(id);
    changed = true;
    current = kids[0];
  }
  return changed;
}

async function expandSubtree(node) {
  const queue = [node];
  while (queue.length) {
    const n = queue.shift();
    if (!isDirectory(n)) continue;
    const id = String(n.id);
    if (!expanded.value.has(id)) expanded.value.add(id);
    await loadChildrenIfNeeded(n);
    const kids = getChildren(n);
    for (const k of kids) if (isDirectory(k)) queue.push(k);
  }
}

function collapseSubtree(node) {
  const queue = [node];
  while (queue.length) {
    const n = queue.shift();
    if (!isDirectory(n)) continue;
    const id = String(n.id);
    if (expanded.value.has(id)) expanded.value.delete(id);
    const kids = getChildren(n);
    for (const k of kids) if (isDirectory(k)) queue.push(k);
  }
}

async function expandOrToggleDir(node, altKey) {
  if (altKey) {
    const id = String(node.id);
    if (expanded.value.has(id)) collapseSubtree(node); else await expandSubtree(node);
    return;
  }
  const didChain = await expandCompressedChain(node);
  if (!didChain) {
    const id = String(node.id);
    const wasExpanded = expanded.value.has(id);
    
    if (!wasExpanded) {
      // 展开时先加载子节点，再更新展开状态
      await loadChildrenIfNeeded(node);
      expanded.value.add(id);
    } else {
      // 折叠时直接移除展开状态
      expanded.value.delete(id);
    }
  }
}

function findIndexById(id) {
  const sid = String(id);
  const idx = indexById.value.get(sid);
  return typeof idx === 'number' ? idx : -1;
}

function ensureRowVisible(index) {
  if (virtualListRef.value) {
    virtualListRef.value.ensureIndexVisible(index);
  }
}

async function onKeydown(e) {
  const arr = effectiveFlatNodes.value;
  if (!arr.length) return;
  const curIdx = selectedId.value != null ? findIndexById(selectedId.value) : -1;
  switch (e.key) {
    case 'ArrowDown': {
      const next = Math.min((curIdx < 0 ? 0 : curIdx + 1), arr.length - 1);
      selectedId.value = String(arr[next].node.id);
      ensureRowVisible(next);
      e.preventDefault();
      break;
    }
    case 'ArrowUp': {
      const prev = Math.max((curIdx < 0 ? 0 : curIdx - 1), 0);
      selectedId.value = String(arr[prev].node.id);
      ensureRowVisible(prev);
      e.preventDefault();
      break;
    }
    case 'ArrowRight': {
      if (curIdx < 0) break;
      const item = effectiveFlatNodes.value[curIdx];
      if (item && isDirectory(item.node)) {
        const id = String(item.node.id);
        if (!expanded.value.has(id)) {
          await expandOrToggleDir(item.node, false);
        } else if (curIdx + 1 < effectiveFlatNodes.value.length && effectiveFlatNodes.value[curIdx + 1].depth > item.depth) {
          selectedId.value = String(effectiveFlatNodes.value[curIdx + 1].node.id);
          ensureRowVisible(curIdx + 1);
        }
        e.preventDefault();
      }
      break;
    }
    case 'ArrowLeft': {
      if (curIdx < 0) break;
      const item = effectiveFlatNodes.value[curIdx];
      if (!item) break;
      const id = String(item.node.id);
      if (isDirectory(item.node) && expanded.value.has(id)) {
        expanded.value.delete(id);
        e.preventDefault();
      } else {
        const pid = nodeParentMap.value.get(id);
        if (pid != null) {
          const pidx = findIndexById(pid);
          if (pidx >= 0) {
            selectedId.value = String(pid);
            ensureRowVisible(pidx);
            e.preventDefault();
          }
        }
      }
      break;
    }
    case 'Enter':
    case ' ': {
      if (curIdx < 0) break;
      const item = arr[curIdx];
      if (isDirectory(item.node)) {
        await expandOrToggleDir(item.node, false);
        e.preventDefault();
      }
      break;
    }
    case 'Home': {
      if (arr.length) {
        selectedId.value = String(arr[0].node.id);
        ensureRowVisible(0);
        e.preventDefault();
      }
      break;
    }
    case 'End': {
      if (arr.length) {
        const last = arr.length - 1;
        selectedId.value = String(arr[last].node.id);
        ensureRowVisible(last);
        e.preventDefault();
      }
      break;
    }
    case 'PageDown': {
      const page = Math.max(1, Math.floor((scrollerRef.value?.clientHeight || 0) / rowHeight) - 1);
      const next = Math.min((curIdx < 0 ? 0 : curIdx + page), arr.length - 1);
      selectedId.value = String(arr[next].node.id);
      ensureRowVisible(next);
      e.preventDefault();
      break;
    }
    case 'PageUp': {
      const page = Math.max(1, Math.floor((scrollerRef.value?.clientHeight || 0) / rowHeight) - 1);
      const prev = Math.max((curIdx < 0 ? 0 : curIdx - page), 0);
      selectedId.value = String(arr[prev].node.id);
      ensureRowVisible(prev);
      e.preventDefault();
      break;
    }
    case 'F2': {
      // rename current selection
      e.preventDefault();
      try { triggerAction('fs:rename'); } catch {}
      break;
    }
    case 'Delete': {
      e.preventDefault();
      try { triggerAction('fs:delete'); } catch {}
      break;
    }
  }
}

// 虚拟滚动数据
const virtualItems = computed(() => {
  return effectiveFlatNodes.value.map((flatNode, index) => ({
    id: `${flatNode.node.id}-${index}`,
    node: flatNode.node,
    depth: flatNode.depth,
    displayLabel: flatNode.displayLabel
  }));
});

function isNodeSelected(id) { return selectedIds.value.has(String(id)); }

// splitForHighlight 函数已移至 TreeRow 组件

function onRowDragStart(row, e) {
  const id = String(row.item.node.id);
  if (!isNodeSelected(id)) {
    selectedIds.value = new Set([id]);
    selectedId.value = id;
    lastAnchorIndex.value = row.index;
    emit('selection-change', Array.from(selectedIds.value));
  }
  const ids = Array.from(selectedIds.value);
  try { e.dataTransfer?.setData('text/plain', JSON.stringify({ ids })); } catch {}
  emit('dragstart', { event: e, ids, node: row.item.node });
}
function onRowDragOver(row, e) { e.preventDefault(); emit('dragover', { event: e, node: row.item.node }); }
function onRowDragLeave(row, e) { emit('dragleave', { event: e, node: row.item.node }); }
function onRowDrop(row, e) {
  e.preventDefault();
  const ids = Array.from(selectedIds.value);
  emit('drop', { event: e, target: row.item.node, ids });
}
function onRowDragEnd(row, e) { emit('dragend', { event: e }); }

function onRowDblClick(row, e) {
  const node = row?.item?.node;
  if (!node) return;
  const isDir = node.isDir === true || node.is_dir === true || node.isFolder === true;
  if (isDir) {
    Promise.resolve(expandOrToggleDir(node, !!e.altKey));
  } else {
    emit('open', node);
  }
}

// VirtualList内部处理滚动和尺寸监听
onMounted(() => {
  if (props.root) {
    loadChildrenIfNeeded(props.root);
    if (props.showRoot && props.root.id != null) expanded.value.add(String(props.root.id));
  }
});

watch(() => props.root && props.root.id, (id) => {
  if (!id) return;
  if (props.showRoot) expanded.value.add(String(id));
  // 根切换时，清空索引避免陈旧引用
  try { nodeIndex.value.clear(); } catch {}
});

// onBeforeUnmount 清理逻辑已移至 VirtualList 组件

function onScroll(newScrollTop) {
  scrollTop.value = newScrollTop;
}
</script>

<template>
  <div ref="rootRef" class="treeview relative text-[12px] leading-[24px] text-slate-700 dark:text-neutral-300 min-w-0 h-full">
    <VirtualList
      ref="virtualListRef"
      :items="virtualItems"
      :item-height="rowHeight"
      :overscan="5"
      class="h-full focus:outline-none focus:ring-1 focus:ring-blue-400/40 rounded-sm thin-scrollbar scrollbar-compact"
      tabindex="0"
      role="tree"
      @keydown="onKeydown"
      @scroll="onScroll"
    >
      <template #default="{ item, index }">
        <TreeRow
          :item="item"
          :index="index"
          :indent="indent"
          :row-height="rowHeight"
          :is-expanded="isExpanded"
          :is-selected="(id) => selectedId === String(id)"
          :is-node-selected="isNodeSelected"
          :selected-id="selectedId"
          :editing-node-id="editingNodeId"
          :editing-mode="editingMode"
          :editing-label="editingLabel"
          :filter-text="filterText"
          :compress-separator="compressSeparator"
          :animate-changes="animateChanges"
          :draggable="draggable"
          :t="t"
          @click="({ node, event }) => { select(node, event); const isDir = node.isDir === true || node.is_dir === true || node.isFolder === true; if (!isDir) emit('preview', node); }"
           @dblclick="({ node, event }) => onRowDblClick({ item: { node } }, event)"
           @contextmenu="({ node, event }) => select(node, event, { context: true })"
           @toggle="({ node }) => toggle(node)"
           @load-children="({ node }) => loadChildrenIfNeeded(node)"
           @dragstart="({ node, event, index: rowIndex }) => onRowDragStart({ item: { node }, index: rowIndex }, event)"
           @dragover="({ node, event, index: rowIndex }) => onRowDragOver({ item: { node }, index: rowIndex }, event)"
           @dragleave="({ node, event, index: rowIndex }) => onRowDragLeave({ item: { node }, index: rowIndex }, event)"
           @drop="({ node, event, index: rowIndex }) => onRowDrop({ item: { node }, index: rowIndex }, event)"
           @dragend="({ node, event, index: rowIndex }) => onRowDragEnd({ item: { node }, index: rowIndex }, event)"
          @editing-input="setEditingInput"
          @commit-inline="commitInline"
          @cancel-inline="cancelInline"
          @update:editing-label="editingLabel = $event"
        >
          <template #prefix="{ node, depth }">
            <slot name="prefix" :node="node" :depth="depth"></slot>
          </template>
          <template #suffix="{ node, depth }">
            <slot name="suffix" :node="node" :depth="depth"></slot>
          </template>
        </TreeRow>
      </template>
    </VirtualList>
  </div>
</template>

<script>
export default { name: 'TreeView' };
</script>

<style scoped>
.treeview { user-select: none; }

/* 行级淡入动画，用于刷新/rehydrate 时减少突兀变化 */
.tv-anim { animation: tv-fade-in 140ms ease; will-change: opacity, transform; }
@keyframes tv-fade-in {
  from { opacity: 0.01; transform: translateY(2px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .tv-anim { animation: none; }
}
</style>




